/**
 * Copyright (C) @2014 Webank Group Holding Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing permissions and limitations under the License.
 */

package cn.webank.framework.biz.service.support;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderSupport;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FileCopyUtils;

import cn.webank.framework.biz.annotation.WeBankMessageObject;
import cn.webank.framework.biz.annotation.WeBankMessageParamer;
import cn.webank.framework.biz.annotation.WeBankMessageService;
import cn.webank.framework.biz.annotation.WebankInputValidation;
import cn.webank.framework.biz.annotation.WebankRequestValidation;
import cn.webank.framework.biz.dto.ServiceConfig;
import cn.webank.framework.biz.service.WeBankBaseService;
import cn.webank.framework.dto.BizErrors;
import cn.webank.framework.dto.MonitorKeys;
import cn.webank.framework.dto.ResponseKeys;
import cn.webank.framework.dto.ValidationTypeEnum;
import cn.webank.framework.exception.SysException;
import cn.webank.framework.mapper.JsonMapper;
import cn.webank.framework.utils.WeBankContextUtil;
import cn.webank.rmb.message.Message;

/**
 * 服务分发类
 * 
 * @author jonyang
 * @author calmanpan
 *
 */
@Service("cn.webank.framework.biz.service.support.WeBankServiceDispatcher")
public class WeBankServiceDispatcher implements InitializingBean, ApplicationContextAware {

	private final static Logger MONITOR_LOGGER = LoggerFactory.getLogger(MonitorKeys.APP_MONITOR_LOGGER);

	private final static Logger LOG = LoggerFactory.getLogger(WeBankServiceDispatcher.class);

	private ApplicationContext context;

	private final Map<String, List<ServiceConfig>> serviceMapping = new HashMap<String, List<ServiceConfig>>();

	private static JsonMapper jsonMapper = JsonMapper.nonEmptyMapper();

	public final static String PROPERTY_THIS_OBJECT = "this";
	public final static String PROPERTY_SCENARIO = "scenario";
	public final static String PROPERTY_ORGANIZATIONID = "organizationId";
	public final static String PROPERTY_APPHEADER = "appHeader";
	public final static String PROPERTY_SYSHEADER = "sysHeader";
	private final static String CHARSET_UTF8 = "Utf-8";
	public final static String INTTEST_ENABLE_KEY = "inttest.record.enable";
	public final static String INTTEST_PATH_KEY = "inttest.record.path";
	private final static String PROPERTIES_POSTFIX = ".properties";

	// @Autowired
	// private Environment env;

	// @Value("${inttest.record.enable}")
	private boolean intTestRecordEnable = false;

	// @Value("${inttest.record.path}")
	private String intTestRecordPath = "";

	/*
	 * 设置Spring context
	 * 
	 * @see
	 * org.springframework.context.ApplicationContextAware#setApplicationContext
	 * (org.springframework.context.ApplicationContext)
	 */
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}

	// @Override
	// public void setEnvironment(Environment environment) {
	// this.env = environment;
	// }

	/**
	 * 获取服务配置
	 *
	 * @param serviceId
	 *            服务ID
	 * @param version
	 *            服务版本号
	 * @return 服务配置
	 */
	public ServiceConfig getServiceConfig(String serviceId, String version) {
		List<ServiceConfig> list = serviceMapping.get(serviceId);
		if (CollectionUtils.isEmpty(list))
			return null;

		if (StringUtils.isEmpty(version)) {
			return list.get(0);
		}

		for (ServiceConfig sc : list) {
			String minVersion = sc.getMinVersion();
			String maxVersion = sc.getMaxVersion();
			String newVersion = version + "0";// matcher
			if ((StringUtils.isNoneEmpty(minVersion) && newVersion.compareTo(minVersion) < 0)
					|| (StringUtils.isNoneEmpty(maxVersion) && newVersion.compareTo(maxVersion) > 0)) {
				// 版本检查不通过，继续查找
				continue;
			} else {
				return sc;
			}
		}

		return null;
	}

	/**
	 * for test
	 * 
	 * @param serviceId
	 *            服务ID
	 * @param config
	 *            服务配置
	 */
	public void addServiceConfig(String serviceId, ServiceConfig config) {
		List<ServiceConfig> list = serviceMapping.get(serviceId);
		if (list == null) {
			list = new ArrayList<ServiceConfig>();
			serviceMapping.put(serviceId, list);
		}
		list.add(config);

	}

	/**
	 * 服务分发
	 * 
	 * @param organizationId
	 *            法人号
	 * 
	 * @param serviceId
	 *            服务ID
	 * @param scenario
	 *            场景号
	 * @param version
	 *            服务版本号
	 * @param rmbMessage
	 *            请求消息
	 * @param errors
	 *            业务异常
	 * @param <T>
	 *            业务对象类型
	 * @return 业务对象
	 */
	@SuppressWarnings("unchecked")
	public <T> T dispatch(String organizationId, String serviceId, String scenario, String version,
			final Message rmbMessage, // String
										// jsonMessageStr,
			BizErrors errors) {

		String jsonMessageStr = rmbMessage.getContent();
		ServiceConfig serviceConfig = getServiceConfig(serviceId, version);
		// 前端根据此code的后四位来构造请求响应，提醒更新组件
		if (serviceConfig == null) {
			String systemId = WeBankContextUtil.getSysId();
			errors.reject(systemId + ResponseKeys.SERVICE_NO_DEFINE_CODE, null,
					"service:" + serviceId + "(" + version + ") config is null");
			return null;
		}

		if (this.intTestRecordEnable) {
			File parentPath = new File(this.intTestRecordPath);
			if (!parentPath.exists()) {
				parentPath.mkdirs();
			}

			String bizSeqNo = WeBankContextUtil.getBizSeqNo();
			File inputFile = new File(this.intTestRecordPath + File.separator + serviceId + "_" + version + "_"
					+ scenario + "_" + bizSeqNo + "_input.json");
			try {
				FileCopyUtils.copy(jsonMessageStr.getBytes(), inputFile);
			} catch (IOException e) {

			}
		}

		// if (serviceConfig == null) {
		// throw new SysException("service:" + serviceId + " config is null");
		// }

		Class<?> requestMessageClass = serviceConfig.getRequestMessageClass();

		// 校验参数输入
		if (requestMessageClass.isAnnotationPresent(WebankRequestValidation.class)) {

			Object request = jsonMapper.fromJson(jsonMessageStr, requestMessageClass);

			Field[] fieldNames = requestMessageClass.getDeclaredFields();
			for (Field field : fieldNames) {
				if (field.isAnnotationPresent(WebankInputValidation.class)) {
					WebankInputValidation validation = field.getAnnotation(WebankInputValidation.class);
					ValidationTypeEnum type = validation.type();
					if (ValidationTypeEnum.NOTNULL.equals(type)) {
						try {

							BeanWrapper wrapper = new BeanWrapperImpl(request);
							@SuppressWarnings("rawtypes")
							Class clazz = wrapper.getPropertyType(field.getName());
							Object o = wrapper.getPropertyValue(field.getName());

							if (o == null || (clazz == String.class && StringUtils.isBlank((String) (o)))) {
								errors.reject(validation.errCode(), null, validation.errMsg());
								return null;
							}
						} catch (IllegalArgumentException e) {
							errors.reject(ResponseKeys.VALIDATION_ERROR_CODE, null,
									field.getName() + "validation exception");
							return null;
						}
					} else if (ValidationTypeEnum.SPECIFIC.equals(type)) {
						// call specific validation function
						String fieldName = field.getName();
						String methodName = "check" + fieldName.replaceFirst(fieldName.substring(0, 1),
								fieldName.substring(0, 1).toUpperCase());
						try {

							BeanWrapper wrapper = new BeanWrapperImpl(request);
							@SuppressWarnings("rawtypes")
							Class clazz = wrapper.getPropertyType(field.getName());
							Object o = wrapper.getPropertyValue(field.getName());

							Method method = requestMessageClass.getMethod(methodName, clazz);
							boolean result = (boolean) method.invoke(request, o);
							if (!result) {
								errors.reject(validation.errCode(), null, validation.errMsg());
								return null;
							}
						} catch (NoSuchMethodException | SecurityException | IllegalAccessException
								| IllegalArgumentException | InvocationTargetException | ClassCastException
								| NullPointerException e) {
							e.printStackTrace();
							errors.reject(ResponseKeys.VALIDATION_ERROR_CODE, null,
									field.getName() + "validation exception");
							return null;
						}

					}

				}

			}

		}

		Method method = serviceConfig.getMethod();
		WeBankBaseService service = serviceConfig.getService();
		List<String> fieldNames = serviceConfig.getFieldNames();

		Object message = jsonMapper.fromJson(jsonMessageStr, requestMessageClass);

		// 默认service最后一个参数为BizErrors
		Object[] params = new Object[(fieldNames == null ? 1 : fieldNames.size() + 1)];
		int i = 0;
		if (fieldNames.size() > 0) {
			for (String f : fieldNames) {
				try {
					Object obj = null;
					if (f.equals(PROPERTY_THIS_OBJECT)) {
						obj = message;
					} else if (f.equals(PROPERTY_SCENARIO)) {
						obj = scenario;
					} else if (f.equals(PROPERTY_ORGANIZATIONID)) {
						obj = organizationId;
					} else if (f.equals(PROPERTY_APPHEADER)) {
						obj = rmbMessage.getAppHeader();
					} else if (f.equals(PROPERTY_SYSHEADER)) {
						obj = rmbMessage.getSysHeader();
					} else {
						if (message != null && f != null) {
							obj = PropertyUtils.getProperty(message, f);
						} else {
							obj = null;
						}
					}
					params[i++] = obj;
				} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
					throw new SysException("get object property error", e);
				}
			}
		}

		params[i] = errors;

		try {
			long beginTime = System.currentTimeMillis();
			Object returnObject = method.invoke(service, params);
			String formatMonitorMsg = String.format(MonitorKeys.REQUEST_SUCCESS_TEMPLATE, service.getClass().getName(),
					method.getName(), WeBankContextUtil.getBizSeqNo(), "" + (System.currentTimeMillis() - beginTime),
					MonitorKeys.SUCCESS_FLAG);
			MONITOR_LOGGER.info(formatMonitorMsg);

			if (this.intTestRecordEnable) {
				String bizSeqNo = WeBankContextUtil.getBizSeqNo();
				String json = jsonMapper.toJson(returnObject);
				File outputFile = new File(this.intTestRecordPath + File.separator + serviceId + "_" + version + "_"
						+ scenario + "_" + bizSeqNo + "_output.json");
				try {
					FileCopyUtils.copy(json.getBytes(), outputFile);
				} catch (IOException e) {

				}
			}

			return (T) returnObject;
		} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
			String formatMonitorMsg = String.format(MonitorKeys.REQUEST_FAILURE_TEMPLATE, service.getClass().getName(),
					method.getName(), WeBankContextUtil.getBizSeqNo(), MonitorKeys.FAILURE_FLAG);
			MONITOR_LOGGER.info(formatMonitorMsg);

			throw new SysException("service execute error", e);
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */

	@Override
	public void afterPropertiesSet() throws Exception {
		// 获得所有BasicHandler接口所有Bean
		Map<String, WeBankBaseService> allServices = this.context.getBeansOfType(WeBankBaseService.class);

		if (allServices == null) {
			return;
		}

		for (WeBankBaseService service : allServices.values()) {
			// class
			Class<?> wbServiceClaz = service.getClass();
			populuateClasses(wbServiceClaz, service);

			// interface
			wbServiceClaz = service.getClass();
			populateInterfaces(wbServiceClaz, service);

		}

		LOG.debug("serviceMapping:" + jsonMapper.toJson(serviceMapping));

		initForIntTest();

	}

	private void initForIntTest() {
		InnerPropertiesLoaderSupport pls = new InnerPropertiesLoaderSupport();
		pls.setFileEncoding(CHARSET_UTF8);
		ClassPathResource cpr = new ClassPathResource(".");
		Properties props = new Properties();
		try {
			String path = cpr.getURL().getPath();
			File f = new File(path);
			FilenameFilter fnameFilter = new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {
					return name.endsWith(PROPERTIES_POSTFIX);
				}
			};

			String[] fileList = f.list(fnameFilter);

			if (fileList != null) {
				int length = fileList.length;
				Resource[] locations = new Resource[length];
				for (int i = 0; i < length; i++) {
					locations[i] = (new ClassPathResource(fileList[i]));
				}

				pls.setLocations(locations);
				pls.loadProperties(props);
			}

		} catch (IOException e) {

		}

		String enable = props.getProperty(INTTEST_ENABLE_KEY);
		if (enable != null) {
			this.intTestRecordEnable = Boolean.valueOf(enable);
		}

		String path = props.getProperty(INTTEST_PATH_KEY);
		if (path != null) {
			this.intTestRecordPath = path;
		}

		LOG.debug("" + INTTEST_PATH_KEY + ":" + enable + "," + INTTEST_PATH_KEY + ":" + path);

	}

	private void populuateClasses(Class<?> claz, WeBankBaseService service) {
		Class<?> wbServiceClaz = claz;
		while (WeBankBaseService.class.isAssignableFrom(wbServiceClaz)) {
			populateClassServiceConfig(wbServiceClaz, service);
			wbServiceClaz = wbServiceClaz.getSuperclass();
		}
	}

	private void populateInterfaces(Class<?> claz, WeBankBaseService service) {
		Class<?>[] interfaces = claz.getInterfaces();
		if (interfaces != null) {
			for (Class<?> ci : interfaces) {
				Class<?> wbServiceIntf = ci;
				if (WeBankBaseService.class.isAssignableFrom(wbServiceIntf)) {
					populateClassServiceConfig(wbServiceIntf, service);
					Class<?>[] interfaces2 = wbServiceIntf.getInterfaces();
					for (Class<?> ci2 : interfaces2) {
						// 递归
						populateInterfaces(ci2, service);
					}
				}
			}
		}
	}

	private void populateClassServiceConfig(Class<?> serviceClaz, WeBankBaseService service) {

		Method methods[] = serviceClaz.getMethods();
		if (methods == null) {
			return;
		}

		for (Method m : methods) {
			if (m.isAnnotationPresent(WeBankMessageService.class)) {
				WeBankMessageService serviceAnnotation = m.getAnnotation(WeBankMessageService.class);
				List<String> fieldNames = new ArrayList<String>();
				Annotation[][] parameterAnnotations = m.getParameterAnnotations();
				if (parameterAnnotations == null) {
					continue;
				}

				for (int i = 0; i < parameterAnnotations.length; i++) {
					int length = parameterAnnotations[i].length;
					for (int j = 0; j < length; j++) {
						Object annotationObj = parameterAnnotations[i][j];
						String fieldName = null;
						if (annotationObj instanceof WeBankMessageParamer) {
							WeBankMessageParamer wmp = (WeBankMessageParamer) annotationObj;
							fieldName = wmp.value();
						} else if (annotationObj instanceof WeBankMessageObject) {
							fieldName = PROPERTY_THIS_OBJECT;
						} else {
							throw new SysException("parameter annotation error");
						}

						// 增加至绑定参数列表
						fieldNames.add(fieldName);
					}
				}

				ServiceConfig serviceConfig = new ServiceConfig(service, m, serviceAnnotation.requestMessageClass(),
						fieldNames);

				String[] serviceIds = serviceAnnotation.serviceIds();
				populateVersion(serviceConfig, serviceAnnotation);

				for (String sid : serviceIds) {
					this.addServiceConfig(sid, serviceConfig);
				}

			} // end if
		} // end for methods
	}

	private static void populateVersion(ServiceConfig sc, WeBankMessageService serviceAnnotation) {
		String version = serviceAnnotation.version();
		String minVersion = "1.0.00";
		String maxVersion = "1.0.00";

		if (version == null) {
			sc.setMinVersion(minVersion);
			sc.setMaxVersion(maxVersion);
			return;
		}

		Pattern p1 = Pattern.compile("\\w+\\.\\w+\\.\\w+");
		Matcher matcher1 = p1.matcher(version);
		if (matcher1.find()) {
			minVersion = version + "0";
			maxVersion = version + "0";
			sc.setMinVersion(minVersion);
			sc.setMaxVersion(maxVersion);
			return;
		}

		Pattern p2 = Pattern.compile("(\\[|\\({1,1})(\\w+\\.\\w+\\.\\w+)\\,(\\w+\\.\\w+\\.\\w+)(\\]|\\){1,1})");
		Matcher matcher2 = p2.matcher(version);

		if (matcher2.find()) {
			String prefix = matcher2.group(1);
			String min = matcher2.group(2);
			String max = matcher2.group(3);
			String postfix = matcher2.group(4);

			minVersion = min + (prefix.equals("[") ? "0" : "1");
			maxVersion = max + (postfix.equals("]") ? "0" : "1");
			sc.setMinVersion(minVersion);
			sc.setMaxVersion(maxVersion);
			return;
		}

		sc.setMinVersion(minVersion);
		sc.setMaxVersion(maxVersion);
	}

	/**
	 * @return the intTestRecordPath
	 */
	public String getIntTestRecordPath() {
		return intTestRecordPath;
	}

	/**
	 * @param intTestRecordPath
	 *            the intTestRecordPath to set
	 */
	public void setIntTestRecordPath(String intTestRecordPath) {
		this.intTestRecordPath = intTestRecordPath;
	}

	/**
	 * @return the intTestRecordEnable
	 */
	public boolean isIntTestRecordEnable() {
		return intTestRecordEnable;
	}

	/**
	 * @param intTestRecordEnable
	 *            the intTestRecordEnable to set
	 */
	public void setIntTestRecordEnable(boolean intTestRecordEnable) {
		this.intTestRecordEnable = intTestRecordEnable;
	}

	private static class InnerPropertiesLoaderSupport extends PropertiesLoaderSupport {

		public void loadProperties(Properties props) throws IOException {
			super.loadProperties(props);
		}
	}

}
